prostore
Prostore - это библиотека для работы с данными во фронтенде,
вдохновленная apollo-client.
Apollo Client решает только задачи работы с API-запросами GraphQL,
в то время как Prostore позиционируется как общее расширяемое решение,
позволяющее также работать с чисто фронтендовыми данными.
В этом плане Prostore чем-то похож на mobx.
Prostore родился из необходимости быстрого решения частых задач
в SPA проектах, написанных на React с использованием React Hooks,
поэтому на данный момент код библиотеки ориентирован именно
на работу с React.
Эти задачи включают в себя:
-
Создание цепочек зависимостей по данным
-
Запрос данных с бекенда с автоматическим обновлением компонента
при их получении и обработкой индикатора загрузки и ошибок
-
Отправка данных на бекенд в виде императивных мутирующих операций,
с отслеживанием состояния запроса
-
Синхронизация данных с localStorage
Установка
yarn add @proscom/prostore rxjs
//
npm install --save @proscom/prostore rxjs
Сторы
Данные в Prostore хранятся в сторах. Каждый стор представляет собой
некое хранилище, выполняющее какую-то одну функцию. Например,
стором может быть состояние выполнения API-запроса, или данные
о текущем пользователе.
В основе Prostore лежит RxJS -
мощная библиотека для работы с Observable.
Observable это как EventEmitter, только с одним типом событий - обновление данных.
Но благодаря функциям-операторам из rxjs можно создавать цепочки
зависимостей одних Observable от других.
Поближе познакомиться с rxjs можно здесь.
Каждый стор в Prostore реализует следующий интерфейс:
export interface IStore<State> {
readonly state: State;
readonly state$: Observable<State>;
}
Это позволяет создавать из сторов цепочки зависимостей. Например,
при обновлении данных о текущем пользователе можно автоматически
перевыполнить API-запрос, зависящий от них.
Также в любой момент можно получить актуальное состояние стора.
BehaviorStore
Для удобства есть базовый класс BehaviorStore
у которого state$
представляет собой BehaviorSubject
из rxjs.
Расширив этот класс, можно создавать свои собственные сторы, у которых
работа с состоянием похожа на классовые компоненты в React. Например,
import { BehaviorStore } from '@proscom/prostore';
class UserStore extends BehaviorStore {
constructor() {
super({
user: null
});
}
updateUser(newUser) {
this.setState({ user: newUser });
}
}
При расширении BehaviorStore в конструктор базового класса надо
передать первоначальное состояние стора - любой JS объект.
Это должен быть именно объект. Состояние не может быть массивом или простым типом.
Поэтому если надо использова не-объект, то оберните его в объект, присвоив
какому-нибудь ключу:
super({
data: [1, 2, 3]
});
В любом месте этого класса (а также снаружи, но это не рекомендуется)
можно вызывать функцию this.setState
, которая принимает обновление
состояния, либо функцию обновления состояния (как в реакте).
При вызове this.setState
происходит одноуровневое слияние старого
состояния с новым (типа newState = {...oldState, ...changes}
).
Если же передана функция, то она сразу вызывается и в аргумент ей
передается текущее состояние, а вернуть она должна изменения.
Если нужно сбросить состояние целиком, например, чтобы удалить какие-то ключи,
можно воспользоваться более низкоуровневым вызовом this.state$.next(newState)
.
Создав такой стор, можно дальше подписаться на него стандартными
средствами rxjs:
const userStore = new UserStore();
const subscription = userStore.state$.subscribe((state) => {
console.log('state changed', state);
});
userStore.updateUser('Tester');
subscription.unsubscribe();
https://codesandbox.io/s/prostore-example-behavior-jomsr
Для удобства работы с подписками в React, смотри библиотеку prostore-react
.
AsyncBehaviorStore
Иногда может быть полезно не применять изменения состояния стора
сразу, а отложить их до следующего цикла. Так например делает React
при работе с классовыми компонентами.
В Prostore есть класс AsyncBehaviorStore, который собирает все вызовы
this.setState
в текущем синхронном цикле и выполняет их все сразу
последовательно в следующем (с помощью setTimeout
).
Это может быть полезно, если стор может измениться более одного раза за
синхронный цикл. С точки зрения подписчиков, AsyncBehaviorStore изменится
только один раз, в то время как BehaviorStore вызовет своих подписчиков
при каждом изменении.
import { AsyncBehaviorStore } from '@proscom/prostore';
class AsyncUserStore extends AsyncBehaviorStore {
constructor() {
super({
firstName: null,
lastName: null
});
}
setFirstName(firstName) {
this.setState({ firstName });
}
setLastName(lastName) {
this.setState({ lastName });
}
}
const userStore = new AsyncUserStore();
userStore.state$.subscribe((state) => {
console.log('state changed', state);
});
userStore.setFirstName('first');
userStore.setLastName('last');
https://codesandbox.io/s/prostore-example-async-kwnns
RequestStore
RequestStore это более высокоуровневая абстракция, которая представляет
собой состояние какого-либо запроса. Запрос - это произвольная функция,
возможно асинхронная, которая превращает свои параметры variables
в результат data
. Например, эта функция может выполнять GET HTTP-запрос
с помощью fetch, передавая variables
как query-параметры, и сохранять
тело результата как data
.
Состояние RequestStore имеет следующий тип:
export interface IRequestState<Vars, Data> {
data: Data | null;
loading: boolean;
loaded: boolean;
error: any;
variables: Vars | null;
}
У RequestStore есть основной метод, который можно вызывать снаружи:
async function loadData(
variables: Vars,
options: any = {}
): Promise<IRequestState<Vars, Data>> {}
При вызове этой функции запускается выполнение нового запроса данных
с новыми variables. Функция завершается, когда запрос будет выполнен
успешно либо с ошибкой.
Для создания собственного стора надо расширить класс RequestStore
,
переопределив функцию
export type IObservableData<Data> = Promise<Data> | Observable<Data>;
function performRequest(
variables: Vars,
options: Options
): IObservableData<Data>;
Пример можно посмотреть на CodeSandbox:
https://codesandbox.io/s/prostore-example-request-h9641
В библиотеках prostore-apollo
и prostore-axios
доступны свои классы,
расширяющие RequestStore, реализующие GraphQL-запросы и обычные HTTP-запросы
соответственно.
При вызове конструктора RequestStore
необходимо передать аргумент типа:
export interface IRequestStoreParams<Vars, Data> {
initialData?: Data;
skipQuery?: ISkipQueryFn<Vars, Data>;
updateData?: IUpdateDataFn<Vars, Data>;
ssrId?: string;
}
export type ISkipQueryFn<Vars, Data> = (vars: Vars) => Data | null | undefined;
export type IUpdateDataFn<Vars, Data> = (
data: Data,
oldData: Data,
params: { store: any; variables: Vars; options: any }
) => Data;
Для удобства в качестве skipQuery
можно передать одну из двух
предопределенных функций:
import { skipIf, skipIfNull } from '@proscom/prostore';
new MyRequestStore({
skipQuery: skipIf((vars) => !vars, defaultData),
skipQuery: skipIfNull(defaultData)
});